package com.lime49.lockcrypt;
import com.lime49.lockcrypt.ui.AbstractDialog;
import com.lime49.lockcrypt.ui.GradientJPanel;
import java.util.*;
import javax.swing.*;
import javax.swing.border.*;
import java.awt.*;
import java.awt.event.*;
import java.awt.datatransfer.Clipboard;
import java.awt.datatransfer.StringSelection;
import java.security.NoSuchAlgorithmException;
import java.net.URL;
import java.security.SecureRandom;
import java.text.MessageFormat;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;
import javax.swing.event.ListSelectionEvent;
import javax.swing.event.ListSelectionListener;

public class FrmPWGen extends AbstractDialog {
    private JButton btnGen, btnCopy, btnClose;
    private JList log;
    private DefaultListModel logModel;
    private JSpinner spnLength;
    private JCheckBox chkUppercase, chkLowercase, chkDigits, chkSpecial, chkBrackets, chkExtended, chkSpecialFormat;
    private JTextField txtSpecialFormat;
    private ResourceBundle lang;
    private Font boldFont;
    private String selectedPassword = "";
    private GradientJPanel pnlMaxEntropy, pnlSelectedEntropy;
    private JLabel lblMaxEntropyPWMeter, lblMaxEntropyBitStrength, lblSelectedEntropyPWMeter, lblSelectedEntropyBitStrength;
    
    private String DIGITS = "0123456789",
                   LOWERCASE = "abcdefghijklmnopqrstuvwxyz",
                   UPPERCASE = "ABCDEFGHIJKLMNOPQRSTUVWXYZ",
                   SPECIAL = "!\"£$€%^&*-_=+@#~?`/\\",
                   BRACKETS = "()[]{}<>",
                   EXTENDED;
    
    /**
     * Creates a Password Generator window, using the specified ResourceBundle for language strings
     * @param owner The parent dialog on which this dialog should be centered.
     * @param lang The ResourceBundle to use for languages
     */
     public FrmPWGen(JDialog owner, ResourceBundle lang) {
         super(owner, lang.getString("PWGEN"), true);
         this.init(lang);
     }
    
     /**
     * Creates a Password Generator window, using the specified ResourceBundle for language strings
     * @param owner The parent dialog on which this dialog should be centered.
     * @param lang The ResourceBundle to use for languages
     */
     public FrmPWGen(JFrame owner, ResourceBundle lang) {
         super(owner, lang.getString("PWGEN"), true);
         this.init(lang);
     }
     
     
     private void init(final ResourceBundle lang) {
         //TODO: move buttons to the bottom and add 'enable secure clipboard' checkbox
         this.lang = lang;
         StringBuffer buff = new StringBuffer(); // populate the extended characters list (mostly unprintable)
         for(int i=161;i<256;i++) {
             buff.append((char)i);
         }
         EXTENDED = buff.toString();
             KeyStroke escapeKeyStroke = KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0, false);
             Action escapeAction = new AbstractAction() {
                 // close the frame when the user presses escape
                 public void actionPerformed(ActionEvent e) {
                     thisDialog.dispose();
                 }
             }; 
             this.getRootPane().getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put(escapeKeyStroke, "ESCAPE");
             this.getRootPane().getActionMap().put("ESCAPE", escapeAction);
             this.setLayout(new BorderLayout());
             this.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);
         JPanel content = new JPanel();
             content.setLayout(new BoxLayout(content, BoxLayout.PAGE_AXIS));
             content.setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 10));
             this.setContentPane(content);
            
        GridBagConstraints c = new GridBagConstraints();
            c.insets = Constants.GLOBALINSETS;
            c.weighty = 1;
            int curY = 0;
            
        JPanel boxLength = new JPanel();
            content.add(boxLength);
            boxLength.setLayout(new BoxLayout(boxLength, BoxLayout.LINE_AXIS));
        JLabel lblLength = new JLabel(lang.getString("LENGTH"));
            boldFont = lblLength.getFont().deriveFont(Font.BOLD);
            lblLength.setFont(boldFont);
            lblLength.setLabelFor(spnLength);
            lblLength.setDisplayedMnemonic('e');
            boxLength.add(Box.createHorizontalStrut(100));
            boxLength.add(lblLength);
            boxLength.add(Box.createHorizontalStrut(10));
        SpinnerModel spnModel  = new SpinnerNumberModel(8,1,511,1);
        spnLength = new JSpinner(spnModel);
                spnLength.setPreferredSize(new Dimension(50,20));
                spnLength.setMaximumSize(new Dimension(80,20));
                spnLength.addChangeListener(new ChangeListener() {
                    public void stateChanged(ChangeEvent e) {
                        calculateMaxEntropy();
                    }
                });
                boxLength.add(spnLength);
        boxLength.add(Box.createHorizontalGlue());
                
        JPanel pnlInclude = new JPanel(new GridLayout(0,2,3,3));
            pnlInclude.setBorder(BorderFactory.createTitledBorder(BorderFactory.createEtchedBorder(EtchedBorder.LOWERED), lang.getString("PWGEN_POSSIBLECHARS")));
            pnlInclude.setFont(boldFont);
            content.add(pnlInclude);
        EntropyChangeListener entropyChangeListener = new EntropyChangeListener();
        MessageFormat fmtUppercase = new MessageFormat(lang.getString("PWGEN_UPPERCASE"));
        chkUppercase = new JCheckBox(fmtUppercase.format(new Object [] {"A-Z"}));
            chkUppercase.setMnemonic('U');
            chkUppercase.setSelected(true);
            chkUppercase.addItemListener(entropyChangeListener);
            pnlInclude.add(chkUppercase);
        MessageFormat fmtDigits = new MessageFormat(lang.getString("PWGEN_DIGITS"));
        chkDigits = new JCheckBox(fmtDigits.format(new Object [] {"0-9"}));
            chkDigits.setMnemonic('D');
            chkDigits.setSelected(true);
            chkDigits.addItemListener(entropyChangeListener);
            pnlInclude.add(chkDigits);
        MessageFormat fmtLowercase = new MessageFormat(lang.getString("PWGEN_LOWERCASE"));
        chkLowercase = new JCheckBox(fmtLowercase.format(new Object [] {"a-z"}));
            chkLowercase.setMnemonic('L');
            chkLowercase.setSelected(true);
            chkLowercase.addItemListener(entropyChangeListener);
            pnlInclude.add(chkLowercase);
        MessageFormat fmtBrackets = new MessageFormat(lang.getString("PWGEN_BRACKETS"));
        chkBrackets = new JCheckBox(fmtBrackets.format(new Object [] {" {},[],(), <> "}));
            chkBrackets.setMnemonic('B');
            chkBrackets.addItemListener(entropyChangeListener);
            pnlInclude.add(chkBrackets);
        chkExtended = new JCheckBox(lang.getString("PWGEN_EXTENDED"));
            chkExtended.setMnemonic('H');
            chkExtended.addItemListener(entropyChangeListener);
            pnlInclude.add(chkExtended);
        MessageFormat fmtSpecial = new MessageFormat(lang.getString("PWGEN_SPECIAL"));
        chkSpecial = new JCheckBox(fmtSpecial.format(new Object [] {"!,\",£,$,%,^", lang.getString("ETC")}));
            chkSpecial.setMnemonic('S');
            chkSpecial.addItemListener(entropyChangeListener);
            pnlInclude.add(chkSpecial);
            
        JPanel pnlFormat = new JPanel(new GridBagLayout());
            curY = 0;
            pnlFormat.setBorder(BorderFactory.createTitledBorder(BorderFactory.createEtchedBorder(EtchedBorder.LOWERED), lang.getString("PWGEN_SPECIALFORMAT")));
            content.add(pnlFormat);
        chkSpecialFormat = new JCheckBox(lang.getString("PWGEN_SPECIALFORMAT"));
    chkSpecialFormat.setBorder(BorderFactory.createLineBorder(Color.black));
            chkSpecialFormat.addActionListener(new ActionListener() {
                public void actionPerformed(ActionEvent e) {
                    if(chkSpecialFormat.isSelected()) {
                        txtSpecialFormat.setEnabled(true);
                        txtSpecialFormat.setBackground(Color.white);
                    } else {
                        txtSpecialFormat.setEnabled(false);
                        txtSpecialFormat.setBackground(Color.getHSBColor(0f, 0f, 0.87f));
                    }
                }
            });
            Utils.setgrid(c,curY,0,1,1,GridBagConstraints.NONE,GridBagConstraints.NORTHWEST,0.8);
            pnlFormat.add(chkSpecialFormat,c);
        JButton btnSpecialFormatInfo = new JButton("");
        final ResourceBundle bundle = lang;
            btnSpecialFormatInfo.setToolTipText(lang.getString("INFORMATION"));
            btnSpecialFormatInfo.addActionListener(new ActionListener() {
                public void actionPerformed(ActionEvent e) {
                    FrmPWGenSpecialFormat frm = new FrmPWGenSpecialFormat(thisDialog, bundle);
                    frm.setLocation(thisDialog.getX() + ((thisDialog.getWidth() - frm.getWidth())/2), thisDialog.getY() + ((thisDialog.getHeight() - frm.getHeight())/2));
                    frm.setVisible(true);
                }
            });
            btnSpecialFormatInfo.setHorizontalAlignment(SwingConstants.LEFT);
            try {
                ImageIcon icoName = new ImageIcon();
                URL URLicon = this.getClass().getResource("images/bulb_16.png");
                icoName.setImage(Toolkit.getDefaultToolkit().getImage(URLicon));
                btnSpecialFormatInfo.setIcon(icoName);
            } catch(Exception e) { }
            Utils.setgrid(c,curY,1,1,1,GridBagConstraints.NONE,GridBagConstraints.NORTHEAST,0.2);
            pnlFormat.add(btnSpecialFormatInfo,c);
            curY++;
        txtSpecialFormat = new JTextField(30);
            txtSpecialFormat.setEnabled(false);
            txtSpecialFormat.setBackground(Color.getHSBColor(0f, 0f, 0.87f));
            Utils.setgrid(c,curY,0,2,1,GridBagConstraints.HORIZONTAL,GridBagConstraints.NORTHWEST,1);
            pnlFormat.add(txtSpecialFormat,c);
            curY++;

        Box pnlGeneratedPasswords = Box.createVerticalBox();
            pnlGeneratedPasswords.setBorder(BorderFactory.createTitledBorder(BorderFactory.createEtchedBorder(EtchedBorder.LOWERED), lang.getString("GENERATEDPASSWORDS")));
            content.add(pnlGeneratedPasswords);
        logModel = new DefaultListModel();
        log = new JList(logModel);
            log.addListSelectionListener(new ListSelectionListener() {
                public void valueChanged(ListSelectionEvent e) {
                    if(!logModel.isEmpty()) {
                        String selectedPassword = log.getSelectedValue().toString();
                        int pwStrength = PWStrengthChecker.checkStrength(selectedPassword);
                        lblSelectedEntropyPWMeter.setText(pwStrength+"% - "+PWStrengthChecker.getStrengthDescription(pwStrength, lang));
                        PWStrengthChecker.setPanelGradient(pnlSelectedEntropy, pwStrength);
                        lblSelectedEntropyBitStrength.setText(PWStrengthChecker.calculateBitStrength(selectedPassword)+" bits");
                    }
                }
            });
            JScrollPane logScroll = new JScrollPane(log);
            log.setSelectionMode(ListSelectionModel.SINGLE_INTERVAL_SELECTION);
            logScroll.setViewportBorder(BorderFactory.createEtchedBorder(EtchedBorder.LOWERED));
            logScroll.setBorder(BorderFactory.createEmptyBorder(3,3,3,3));
            pnlGeneratedPasswords.add(logScroll);
            
        JPanel pnlAnalysis = new JPanel(new GridBagLayout());
            pnlAnalysis.setBorder(BorderFactory.createTitledBorder(BorderFactory.createEtchedBorder(EtchedBorder.LOWERED), lang.getString("PWGEN_ANALYSIS")));
            JButton btnAnalysisInfo = new JButton("");
                btnAnalysisInfo.setToolTipText(lang.getString("INFORMATION"));
                btnAnalysisInfo.addActionListener(new ActionListener() {
                    public void actionPerformed(ActionEvent e) {
                        Utils.openURL("http://wiki.lime49.com/index.php?title=Password_Generator#Password_Analysis");
                    }
                });
                btnAnalysisInfo.setHorizontalAlignment(SwingConstants.LEFT);
                try {
                    ImageIcon icoName = new ImageIcon();
                    URL URLicon = this.getClass().getResource("images/bulb_16.png");
                    icoName.setImage(Toolkit.getDefaultToolkit().getImage(URLicon));
                    btnAnalysisInfo.setIcon(icoName);
                } catch(Exception e) { }
            JLabel lblMaxEntropy = new JLabel(lang.getString("PWGEN_MAXENTROPY"));
            pnlMaxEntropy = new GradientJPanel(new BorderLayout());
                lblMaxEntropyPWMeter = new JLabel("");
                lblMaxEntropyBitStrength = new JLabel("");
                pnlMaxEntropy.setBorder(BorderFactory.createLineBorder(Color.black));
            JLabel lblSelectedEntropy = new JLabel(lang.getString("PWGEN_ENTROPYSELECTED"));
            pnlSelectedEntropy = new GradientJPanel(new BorderLayout());
                curY++;
                lblSelectedEntropyPWMeter = new JLabel(lang.getString("NONESELECTED"));
                lblSelectedEntropyBitStrength = new JLabel("");
                pnlSelectedEntropy.setBorder(BorderFactory.createLineBorder(Color.black));
                calculateMaxEntropy();
                
            content.add(pnlAnalysis);
            curY = 0;
                Utils.setgrid(c,curY,1,1,1,GridBagConstraints.NONE,GridBagConstraints.NORTHEAST,1);
                pnlAnalysis.add(btnAnalysisInfo,c);
                curY++;
                Utils.setgrid(c,curY,0,1,1,GridBagConstraints.NONE,GridBagConstraints.NORTHWEST,0.5);
                pnlAnalysis.add(lblMaxEntropy,c);
                Utils.setgrid(c,curY,1,1,1,GridBagConstraints.HORIZONTAL,GridBagConstraints.NORTHEAST,1);
                pnlAnalysis.add(pnlMaxEntropy,c);
                    pnlMaxEntropy.add(lblMaxEntropyPWMeter, BorderLayout.LINE_START);
                    pnlMaxEntropy.add(lblMaxEntropyBitStrength, BorderLayout.LINE_END);
                curY++;
                Utils.setgrid(c,curY,0,1,1,GridBagConstraints.NONE,GridBagConstraints.NORTHWEST,0.5);
                pnlAnalysis.add(lblSelectedEntropy,c);
                Utils.setgrid(c,curY,1,1,1,GridBagConstraints.HORIZONTAL,GridBagConstraints.NORTHEAST,1);
                pnlAnalysis.add(pnlSelectedEntropy,c);
                    pnlSelectedEntropy.add(lblSelectedEntropyPWMeter, BorderLayout.LINE_START);
                    pnlSelectedEntropy.add(lblSelectedEntropyBitStrength, BorderLayout.LINE_END);
                
        JPanel pnlButtons = new JPanel(new FlowLayout(FlowLayout.TRAILING));
            content.add(pnlButtons);
            pnlButtons.add(Box.createHorizontalGlue());
        btnGen = new JButton(lang.getString("PWGEN_GENERATE"));
            btnGen.setMnemonic('G');
            btnGen.addActionListener(new ActionListener() {
                public void actionPerformed(ActionEvent e) { genPass(); }
            });
            btnGen.setMinimumSize(Constants.MINSIZE);
            btnGen.setMaximumSize(Constants.MAXSIZE);
            pnlButtons.add(btnGen);
            pnlButtons.add(Box.createHorizontalStrut(10));
        btnCopy = new JButton(lang.getString("COPY"));
            btnCopy.setMnemonic('C');
            btnCopy.addActionListener(new ActionListener() {
                public void actionPerformed(ActionEvent e) { copyToClip(); }
            });
            btnCopy.setMinimumSize(Constants.MINSIZE);
            btnCopy.setMaximumSize(Constants.MAXSIZE);
            pnlButtons.add(btnCopy);
            pnlButtons.add(Box.createHorizontalStrut(10));
        btnClose = new JButton(lang.getString("OK"));
            btnClose.addActionListener(new ActionListener() {
                public void actionPerformed(ActionEvent e) {
                    selectedPassword = log.getSelectedValue() == null ? "" : log.getSelectedValue().toString();
                    thisDialog.dispose(); 
                }
            });
            btnClose.setMinimumSize(Constants.MINSIZE);
            btnClose.setMaximumSize(Constants.MAXSIZE);
            pnlButtons.add(btnClose);

        this.setMinimumSize(new Dimension(500,400));
        this.pack();
    }
    
    /**
    * Generates a password according to the selected checkboxes and length spinner
    */
    private void genPass() {
        if(ValidateInput()) {
            int passLength = (Integer)spnLength.getValue();
            StringBuffer chars = new StringBuffer();
            if(chkUppercase.isSelected()) {
                chars.append(UPPERCASE);
            }
            if(chkLowercase.isSelected()) {
                chars.append(LOWERCASE);
            }
            if(chkDigits.isSelected()) {
                chars.append(DIGITS);
            }
            if(chkSpecial.isSelected()) {
                chars.append(SPECIAL);
            }
            if(chkBrackets.isSelected()) {
                chars.append(BRACKETS);
            }
            if(chkExtended.isSelected()) { // 161-255
                chars.append(EXTENDED);
            }
            char[] possChars = chars.toString().toCharArray();
            int numPossChars = possChars.length;
            Random seed = null;
            try {
                seed = SecureRandom.getInstance("SHA1PRNG");
            } catch(NoSuchAlgorithmException e) {
                System.out.println("SHA1PRNG algorithm not found, using standard random generator");
                seed = new Random();
            }
            // Fisher-Yates shuffle
            for(int i=numPossChars-1;i>0;i--) {
                int pos = seed.nextInt(i);
                char temp = possChars[i];
                possChars[i] = possChars[pos];
                possChars[pos] = temp;
            }
            if(Constants.DEBUG) {
              System.out.println(possChars);  
            }
            char[] pass = new char[passLength];
            if(chkSpecialFormat.isSelected()) {
                String format = txtSpecialFormat.getText();
                int i=0;
                while(i<passLength) {
                    char proposed = possChars[seed.nextInt(numPossChars)];
                    if(format.length() > i) {
                        switch(format.charAt(i)) {
                            case 'd':
                                if(DIGITS.indexOf(proposed) >=0) {
                                    pass[i] = proposed;
                                    i++;
                                }
                                break;
                            case 'c':
                                if(LOWERCASE.indexOf(proposed) >=0) {
                                    pass[i] = proposed;
                                    i++;
                                }
                                break;
                            case 'C':
                                if(UPPERCASE.indexOf(proposed) >=0) {
                                    pass[i] = proposed;
                                    i++;
                                }
                                break;
                            case 'b':
                                if(BRACKETS.indexOf(proposed) >=0) {
                                    pass[i] = proposed;
                                    i++;
                                }
                                break;
                            case 's':
                                if(SPECIAL.indexOf(proposed) >=0) {
                                    pass[i] = proposed;
                                    i++;
                                }
                                break;
                            case 'e':
                                if(EXTENDED.indexOf(proposed) >=0) {
                                    pass[i] = proposed;
                                    i++;
                                }
                            default:
                                pass[i] = proposed;
                                i++;
                                break;
                        }
                    } else {
                        pass[i] = proposed;
                        i++;
                    }
                }
            } else {
                for(int i=0;i<passLength;i++) {
                    pass[i] = possChars[seed.nextInt(numPossChars)];
                }
            }

            logModel.addElement(new String(pass));
            log.setSelectedIndex(logModel.getSize()-1);
            log.ensureIndexIsVisible(logModel.getSize()-1);
        }
    }
    
    private void calculateMaxEntropy() {
        int poolsize = 0;
        if(chkUppercase.isSelected()) {
            poolsize += 26;
        }
        if(chkLowercase.isSelected()) {
            poolsize += 26;
        }
        if(chkDigits.isSelected()) {
            poolsize += 10;
        }
        if(chkSpecial.isSelected()) {
            poolsize += 20;
        }
        if(chkBrackets.isSelected()) {
            poolsize += 8;
        }
        if(chkExtended.isSelected()) { // 161-255
            poolsize += 94;
        }
        double bitStrength = Math.log(Math.pow(poolsize, (Integer)spnLength.getValue())) / Math.log(2);
        int score = (int)((double)poolsize/184*100);
        lblMaxEntropyPWMeter.setText(score+"% - "+PWStrengthChecker.getStrengthDescription(score, lang));
        lblMaxEntropyBitStrength.setText((int)Math.round(bitStrength)+" bits");
        PWStrengthChecker.setPanelGradient(pnlMaxEntropy, score);
        lblMaxEntropyPWMeter.invalidate();
        lblMaxEntropyPWMeter.repaint();
        lblMaxEntropyPWMeter.revalidate();
    }
    
    private boolean ValidateInput() {
        boolean havePossible = false;
        if(chkSpecialFormat.isSelected()) {
            String format = txtSpecialFormat.getText();
            boolean haveDigits = (format.indexOf('d') >= 0),
                    haveLowercase = (format.indexOf('c') >= 0),
                    haveUppercase = (format.indexOf('C') >= 0),
                    haveBrackets = (format.indexOf('b') >= 0),
                    haveSpecial = (format.indexOf('s') >= 0),
                    haveExtended = (format.indexOf('e') >= 0),
                    haveAny = (format.indexOf('*') >= 0);
            if(format.length() > 0) {
                if(haveAny) {
                    havePossible = true;
                } else {
                    if(chkDigits.isSelected() && haveDigits) {
                        havePossible = true;
                    } else if(chkLowercase.isSelected() && haveLowercase) {
                        havePossible = true;
                    } else if(chkUppercase.isSelected() && haveUppercase) {
                        havePossible = true;
                    } else if(chkBrackets.isSelected() && haveBrackets) {
                        havePossible = true;
                    } else if(chkSpecial.isSelected() && haveSpecial) {
                        havePossible = true;
                    } else if(chkExtended.isSelected() && haveExtended) {
                        havePossible = true;
                    }
                }
            } 
            if(!havePossible) {
                JOptionPane.showMessageDialog(this, lang.getString("ERR_PWGEN_NOELIGIBLEFORMAT"), lang.getString("ERROR"), JOptionPane.ERROR_MESSAGE);
            }
        } else {
            if(chkDigits.isSelected() || chkLowercase.isSelected()|| chkUppercase.isSelected()|| chkBrackets.isSelected() || chkSpecial.isSelected() || chkExtended.isSelected()) {
                havePossible = true;
            } else {
                JOptionPane.showMessageDialog(this, lang.getString("ERR_PWGEN_NOELIGIBLE"), lang.getString("ERROR"), JOptionPane.ERROR_MESSAGE);
            }
        }
        return havePossible;
    }
    
    /**
     * Returns the selected password, or an empty string if none is selected
     * @return The selected password, or an empty string if none is selected
     */
    public String getSelectedPassword() {
        return selectedPassword;
    }
    
    /**
    * Copys the selected password to the clipboard
    */
    private void copyToClip() {
    //TODO: add secure clipboard, disable button when list empty
        Clipboard clipboard = Toolkit.getDefaultToolkit().getSystemClipboard();
        clipboard.setContents(new StringSelection((String)log.getSelectedValue()), null);
    }
    
    private class EntropyChangeListener implements ItemListener {
        public void itemStateChanged(ItemEvent e) {
            calculateMaxEntropy();
        }
    }
}
